package com.feature50.clarity;

import com.feature50.clarity.binding.ClarityBindingContext;
import com.feature50.clarity.components.DisablePanel;
import com.feature50.clarity.decorator.FormDecorators;
import com.feature50.clarity.formui.NullProgressMonitor;
import com.feature50.clarity.misc.ClarityProgressMonitor;
import com.feature50.clarity.misc.SimpleClarityProgressMonitor;
import com.feature50.util.ReflectionUtils;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import org.apache.log4j.Logger;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.SwingUtilities;
import java.awt.GridLayout;
import java.awt.Window;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FormContainer {
    private static final Logger logger = Logger.getLogger(FormContainer.class);

    private Object lock = new Object();

    private boolean shutdown = false;

    private JRootPane rootPane;
    private JPanel contentPanel;

    private List<Form> stack;

    private Map context;

    private Window window;

    private boolean border = true;

    private DisablePanel disablePanel;
    private PropertyChangeListener simpleMonitorListener;

    public FormContainer(JComponent container, Window parent) {
        window = parent;

        container.setLayout(new GridLayout(1, 1));
        rootPane = new JRootPane();
        contentPanel = new JPanel();
        contentPanel.setLayout(new GridLayout(1, 1));
        rootPane.setContentPane(contentPanel);

        container.add(rootPane);

        stack = new ArrayList<Form>();
        context = new HashMap();
        disablePanel = new DisablePanel(null);
        simpleMonitorListener = new SimpleMonitorListener(disablePanel);
    }

    public Form back() throws FormPassivationException {
        return back(true);
    }

    private Form back(boolean disposeOldForm) throws FormPassivationException {
        if (!SwingUtilities.isEventDispatchThread()) throw new IllegalStateException("This method must be called from the Swing EDT");

        if (shutdown) throw new IllegalStateException("This form container has been shutdown");

        if (stack.size() == 0) return null;

        Form currentForm = null;
        Form previousForm = null;

        synchronized (lock) {
            try {
                currentForm = stack.get(0);
                if (stack.size() > 1) previousForm = stack.get(1);

                passivateForm(currentForm, new PassivateDirection(PassivateDirection.BACKWARDS, previousForm));
                if (previousForm != null) activateForm(previousForm);
            } catch (FormPassivationException e) {
                throw e;
            } catch (Exception e1) {
                logger.error("Couldn't move back", e1);
                throw new FormPassivationException(e1);
            }
            stack.remove(0);
        }

        displayForm();

        if (disposeOldForm) disposeForm(currentForm);

        return currentForm;
    }

    public void show(final Form form) throws FormPassivationException {
        if (!SwingUtilities.isEventDispatchThread()) throw new IllegalStateException("This method must be called from the Swing EDT");

        if (shutdown) throw new IllegalStateException("This form container has been shutdown");

        synchronized (lock) {
            if (stack.indexOf(form) != -1) throw new IllegalArgumentException("The passed form is already on this container's stack");

            if (stack.size() > 0) {
                Form currentForm = stack.get(0);
                passivateForm(currentForm, new PassivateDirection(PassivateDirection.FORWARDS, form));
            }

            if (form.isInitialized()) {
                FormContainer fc = form.formContainer;
                if (fc != null) {
                    int index = fc.stack.indexOf(form);
                    if (index != -1) throw new IllegalStateException("Can't display a form that is already on another stack");
                    form.formContainer = null;
                }
            }

            form.formContainer = FormContainer.this;

            stack.add(0, form);
        }

        Runnable displayForm = new Runnable() {
            public void run() {
                try {
                    if (!form.isInitialized()) {
                        initializeForm(form);
                    }
                } catch (Exception e) {
                    cancelForm(form);

                    throw new FormException("Couldn't initialize form", e);
                }

                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        try {
                            activateForm(form);
                            displayForm();
                        } catch (Exception e) {
                            cancelForm(form);

                            throw new FormException("Couldn't display form", e);
                        }
                    }
                });
            }
        };

        new Thread(displayForm, "Form Container Form Display Thread").start();
    }

    private void cancelForm(Form form) {
        synchronized (lock) {
            int index = stack.indexOf(form);
            if (index != -1) {
                stack.remove(index);

                if (index == 0) {
                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            displayForm();
                        }
                    });
                }
            }
        }
    }

    public Form getCurrentForm() {
        synchronized (lock) {
            return (stack.isEmpty()) ? null : stack.get(0);
        }
    }

    public void disposeForm(Form currentForm) {
        currentForm.dispose();
        currentForm.disposed = true;
    }

    private void initializeForm(final Form form) throws FormException {
        try {
            try {
                SwingUtilities.invokeAndWait(new Runnable () {
                    public void run() {
                        form.formComponent = form.createFormComponent();
                        if (isBorder()) form.formComponent.setBorder(form.getBorder());
                        disableUI(null);
                    }
                });
            } catch (Exception e) {
                throw new FormException("Couldn't perform form initialization", e);
            }

            FormExceptionThread fet = new FormExceptionThread("getData") {
                public void run() {
                    try {
                        SimpleClarityProgressMonitor pm = new SimpleClarityProgressMonitor();
                        pm.addPropertyChangeListener(simpleMonitorListener);
                        disablePanel.setMonitor(pm);
                        form.getData(pm);
                        pm.removePropertyChangeListener(simpleMonitorListener);
                        disablePanel.setMonitor(null);
                    } catch (FormException e) {
                        fe = e;
                    }
                }
            };
            fet.start();

            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        form.createUI(form.formComponent);
                    }
                });
            } catch (Exception e) {
                throw new FormException("Couldn't initialize form", e);
            }

            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        List<Field> fields = new ArrayList<Field>();
                        Class fc = form.getClass();
                        while (true) {
                            Field[] f = fc.getDeclaredFields();
                            if (f != null) fields.addAll(Arrays.asList(f));
                            if (fc.equals(Form.class)) break;
                            fc = fc.getSuperclass();
                        }

                        for (int i = 0; i < fields.size(); i++) {
                            Field field = fields.get(i);
                            if (field.getAnnotation(FormUIField.class) != null) {
                                JComponent component = null;
                                try {
                                    component = form.getComponentByName(field.getName());
                                    if (component != null) {
                                        if (field.getType().isAssignableFrom(component.getClass())) {
                                            field.set(form, component);
                                        }
                                    } else {
                                        logger.error("FormUIField named " + field.getName() + " wasn't present in UI");
                                    }
                                } catch (IllegalAccessException e) {
                                    logger.debug("Couldn't wire up FormUIField " + field.getName() + " due to visibility; attempting to bypass");

                                    try {
                                        field.setAccessible(true);
                                        field.set(form, component);
                                    } catch (Exception e1) {
                                        logger.error("Couldn't bypass visibility restriction for FormUIField " + field.getName() + ", probably due to security manager restrictions", e1);
                                    }
                                } catch (Exception e) {
                                    logger.error("Couldn't wire up FormUIField " + field.getName(), e);
                                }
                            }
                        }
                    }
                });
            } catch (Exception e) {
                throw new FormException("Couldn't wire up variables", e);
            }

            try {
                fet.join();
            } catch (InterruptedException e) {
                logger.warn("Join on the getdata thread was interrupted", e);
            }

            if (fet.getFe() != null) throw fet.getFe();

            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        form.bindingContext = new ClarityBindingContext();
                        form.createBindings(form.bindingContext);

                        form.createActions();

                        form.configureComponents(form.formComponent);

                        form.addListeners(form.formComponent);

                        FormDecorators.getInstance().decorate(form);
                   }
                });
            } catch (Exception e) {
                throw new FormException("Couldn't initialize form", e);
            }

            if (ReflectionUtils.hasMethod("getDataInBackground", new Class[] { ClarityProgressMonitor.class }, Form.class, form.getClass())) {
                Runnable runnable = new Runnable() {
                    public void run() {
                        FormException fe = null;
                        Object data = null;
                        try {
                            data = form.getDataInBackground(new NullProgressMonitor());
                        } catch (FormException e) {
                            fe = e;
                        }

                        final FormException fe1 = fe;
                        final Object data1 = data;
                        SwingUtilities.invokeLater(new Runnable() {
                            public void run() {
                                if (fe1 != null) throw fe1;

                                form.backgroundDataRetrieved(form.formComponent, data1);
                            }
                        });
                    }
                };
                new Thread(runnable, "Form Container \"getDataInBackground\" Thread");
            }
        } catch (FormException e) {
            logger.error("Error occurred whilst initializing form", e);

            form.dispose();

            throw e;
        }
    }

    private void activateForm(final Form form) throws FormException {
        Runnable runnable = new Runnable() {
            public void run() {
                if (!form.isInitialized()) {
                    form.bindingContext.bind();
                    form.setInitialized(true);
                }

                form.activate();
                enableUI();
            }
        };

        if (SwingUtilities.isEventDispatchThread()) {
            runnable.run();
        } else {
            try {
                SwingUtilities.invokeAndWait(runnable);
            } catch (Exception e) {
                throw new FormException(e);
            }
        }
    }

    private void displayForm() {
        if (!SwingUtilities.isEventDispatchThread()) throw new IllegalStateException("This method must be called from the Swing EDT");

        contentPanel.removeAll();

        Form form = null;
        synchronized (lock) {
            if (stack.size() > 0) form = stack.get(0);
        }

        if (form == null) return;

        // populate the content panel
        FormLayout layout = new FormLayout(
                "fill:default:grow",
                "fill:default:grow"
        );
        contentPanel.setLayout(layout);

        CellConstraints cc = new CellConstraints();
        int row = 1;
        contentPanel.add(form.formComponent, cc.xy(1, row++));

        contentPanel.revalidate();
        contentPanel.repaint();

        JComponent component = form.getDefaultFocusComponent();
        if (component != null) component.requestFocus();

        rootPane.setDefaultButton(form.getDefaultButton());

        final Form form1 = form;
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                if (form1.isPackLayoutOnDisplay()) {
                    Window window = form1.getWindow();
                    if (window != null) {
                        window.pack();
                    }
                }

                form1.postDisplay();
            }
        });
    }

    void repaint() {
        rootPane.repaint();
    }

    private void passivateForm(final Form form, PassivateDirection direction) throws FormPassivationException {
        form.passivate(direction);
        logger.debug(String.format("%1$s passivated", form));
    }

    public void shutdown(boolean ignorePassivationExceptions) throws FormPassivationException {
        boolean stackAltered = false;
        synchronized (lock) {
            while (stack.size() > 0) {
                Form form = stack.get(0);

                Form previousForm = null;
                if (stack.size() > 1) {
                    previousForm = stack.get(1);
                }

                try {
                    passivateForm(form, new PassivateDirection(PassivateDirection.BACKWARDS, previousForm));
                } catch (FormPassivationException e) {
                    if (!ignorePassivationExceptions) {
                        if (stackAltered) {
                            try {
                                activateForm(form);
                            } catch (FormException e1) {
                                logger.error("An error occurred whilst activating a form during the shutdown process", e1);
                            }
                            displayForm();
                        }
                        throw e;
                    } else {
                        logger.error("A form passivation exception was thrown and ignored", e);
                    }
                }

                disposeForm(form);
                stack.remove(0);
                stackAltered = true;
            }
        }

        shutdown = true;
        contentPanel.removeAll();
    }

    public void disableUI(SimpleClarityProgressMonitor monitor) {
        monitor.addPropertyChangeListener(simpleMonitorListener);
        disablePanel.setMonitor(monitor);
        rootPane.setGlassPane(disablePanel);
        disablePanel.setBounds(0, 0, rootPane.getWidth(), rootPane.getHeight());
        disablePanel.setVisible(true);
    }

    public void enableUI() {
        ClarityProgressMonitor clarityProgressMonitor = disablePanel.getMonitor();
        if (clarityProgressMonitor instanceof SimpleClarityProgressMonitor) {
            SimpleClarityProgressMonitor m = (SimpleClarityProgressMonitor) clarityProgressMonitor;
            m.removePropertyChangeListener(simpleMonitorListener);
        }

        disablePanel.setVisible(false);
    }

    public Window getWindow() {
        return window;
    }

    public boolean isShutdown() {
        return shutdown;
    }

    public boolean isBorder() {
        return border;
    }

    public void setBorder(boolean border) {
        this.border = border;
    }

    public final Object get(Object key) {
        return get(key);
    }

    public final Object put(Object key, Object value) {
        return context.put(key, value);
    }

    private static class FormExceptionThread extends Thread {
        protected FormException fe = null;

        public FormExceptionThread(String name) {
            super(name);
        }

        public FormException getFe() {
            return fe;
        }
    }

    private static class SimpleMonitorListener implements PropertyChangeListener {
        private JComponent component;

        public SimpleMonitorListener(JComponent component) {
            this.component = component;
        }

        public void propertyChange(PropertyChangeEvent evt) {
            component.repaint();
        }
    }
}